#!/usr/bin/env python3
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import json
import os
import subprocess
import sys
from itertools import chain, product, starmap
from pathlib import Path
from typing import Dict, Iterable, List, NamedTuple

from impl.common import CROSVM_ROOT, Triple, verbose
from impl.test_config import DO_NOT_BUILD_RISCV64

USAGE = """\
Build crosvm with release (optimized) profile.

To target local machine:

    $ ./tools/build_release

To cross-compile for aarch64 or windows you can use:

    $ ./tools/build_release --platform=aarch64
    $ ./tools/build_release --platform=mingw64
"""

# We only need PGO for main binary, but for consistency, only exclude incompatible parts from PGO
PGO_EXCLUDE = ["crosvm_control"]


class Executable(NamedTuple):
    """Container for info about an executable generated by cargo build/test."""

    binary_path: Path
    crate_name: str
    cargo_target: str
    kind: str
    is_test: bool
    is_fresh: bool

    @property
    def name(self):
        return f"{self.crate_name}:{self.cargo_target}"


def cargo(
    cargo_command: str,
    cwd: Path,
    flags: List[str],
    env: Dict[str, str],
) -> Iterable[Executable]:
    """
    Executes a cargo command and returns the list of test binaries generated.

    The build log will be hidden by default and only printed if the build
    fails. In VERBOSE mode the output will be streamed directly.

    Note: Exits the program if the build fails.
    """
    message_format = "json-diagnostic-rendered-ansi" if sys.stdout.isatty() else "json"
    cmd = [
        "cargo",
        cargo_command,
        f"--message-format={message_format}",
        *flags,
    ]
    if verbose():
        print("$", " ".join(cmd))
    process = subprocess.Popen(
        cmd,
        cwd=cwd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        env=env,
    )

    messages: List[str] = []

    # Read messages as cargo is running.
    assert process.stdout
    for line in iter(process.stdout.readline, ""):
        # any non-json line is a message to print
        if not line.startswith("{"):
            if verbose():
                print(line.rstrip())
            messages.append(line.rstrip())
            continue
        json_line = json.loads(line)

        # 'message' type lines will be printed
        if json_line.get("message"):
            message = json_line.get("message").get("rendered")
            if verbose():
                print(message)
            messages.append(message)

        # Collect info about test executables produced
        elif json_line.get("executable"):
            yield Executable(
                Path(json_line.get("executable")),
                crate_name=json_line.get("package_id", "").split(" ")[0],
                cargo_target=json_line.get("target").get("name"),
                kind=json_line.get("target").get("kind")[0],
                is_test=json_line.get("profile", {}).get("test", False),
                is_fresh=json_line.get("fresh", False),
            )

    if process.wait() != 0:
        if not verbose():
            for message in messages:
                print(message)
        sys.exit(-1)


def main():
    parser = argparse.ArgumentParser(usage=USAGE)
    parser.add_argument(
        "--build-target",
        "--platform",
        "-p",
        help=(
            "Override the cargo triple to build. Shorthands are available: (x86_64, "
            + "aarch64, mingw64, msvc64)."
        ),
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output in JSON instead of human readable format.",
    )
    parser.add_argument("--strip", action="store_true", help="Strip output binaries")
    parser.add_argument(
        "--build-profile", help="Select compile profile, default to release.", default="release"
    )
    pgo_group = parser.add_mutually_exclusive_group()
    pgo_group.add_argument(
        "--profile-generate",
        help="Target directory to generate profile when running, must use absolute path",
    )
    pgo_group.add_argument(
        "--profile-use", help="Profile file used for PGO, must use absolute path"
    )
    parser.add_argument("cargo_arguments", nargs="*", help="Extra arguments pass to cargo")

    args = parser.parse_args()

    if args.profile_generate and (
        not os.path.isabs(args.profile_generate) or not os.path.isdir(args.profile_generate)
    ):
        raise ValueError("--profile-generate argument is not an absolute path to a folder")
    if args.profile_use and (
        not os.path.isabs(args.profile_use) or not os.path.isfile(args.profile_use)
    ):
        raise ValueError("--profile-use argument is not an absolute path to a file")

    build_target = Triple.from_shorthand(args.build_target) if args.build_target else None
    build_target = build_target or Triple.host_default()

    exclude_args = [
        f"--exclude={x}" for x in PGO_EXCLUDE if args.profile_generate or args.profile_use
    ]
    if build_target == Triple.from_shorthand("riscv64"):
        exclude_args += ["--exclude=" + s for s in DO_NOT_BUILD_RISCV64]

    features = build_target.feature_flag
    cargo_args = [
        "--profile",
        args.build_profile,
        "--features=" + features,
        f"--target={build_target}",
        "--workspace",
        *exclude_args,
        *args.cargo_arguments,
    ]

    build_env = os.environ.copy()
    build_env.update(build_target.get_cargo_env())
    build_env.setdefault("RUSTFLAGS", "")
    build_env["RUSTFLAGS"] += " -D warnings"
    if args.strip:
        build_env["RUSTFLAGS"] += " -C strip=symbols"
    if args.profile_generate:
        build_env["RUSTFLAGS"] += " -C profile-generate=" + args.profile_generate
    if args.profile_use:
        build_env["RUSTFLAGS"] += " -C profile-use=" + args.profile_use

    executables = list(cargo("build", CROSVM_ROOT, cargo_args, build_env))

    if args.json:
        result = {}
        for exe in executables:
            assert exe.cargo_target not in result
            result[exe.cargo_target] = str(exe.binary_path)
        print(json.dumps(result))
    else:
        print("Release binaries:")
        for exe in executables:
            print(f"Name: {exe.cargo_target}")
            print(f"Path: {str(exe.binary_path)}")
            print()


if __name__ == "__main__":
    main()
